/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.formatter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.lang3.StringUtils;

/**
 * Provides a simple implementation of a formatter.
 */
public final class SimpleFormatter implements Formatter
{
    /** Line separator. */
    private static final String LINE_SEPARATOR = System
            .getProperty("line.separator");

    /** First indent key. */
    private static final String INDENT_KEY0 = "{" + LINE_SEPARATOR;

    /** Second indent key. */
    private static final String INDENT_KEY1 = "}";

    /** Indent. */
    private String _indent1 = "    ";

    /**
     * @see org.jfab.core.formatter.Formatter#format(java.lang.String)
     */
    @Override
    public final String format(final String sourceCode)
    {
        String answer = sourceCode;

        if (StringUtils.isNotEmpty(sourceCode))
        {
            answer = maybeInsertPrecedingReturns(answer, "{");
            answer = maybeInsertFollowingReturns(answer, "{");
            answer = maybeInsertFollowingReturns(answer, "}");
            answer = maybeInsertFollowingReturns(answer, ";");
            answer = maybeInsertPrecedingReturns(answer, "/**");
            answer = maybeInsertFollowingReturns(answer, "*/");

            answer = performIndentation(answer);
        }

        return answer;
    }

    /**
     * @return the indent1
     */
    public final String getIndent1()
    {
        return _indent1;
    }

    /**
     * @param indent1 the indent1 to set
     */
    public final void setIndent1(final String indent1)
    {
        _indent1 = indent1;
    }

    /**
     * Collect occurrences of the given key in the given source code.
     * 
     * @param occurrenceMap Map of index to key.
     * @param sourceCode Source code.
     * @param key Key.
     */
    private void collectOccurrences(final Map<Integer, String> occurrenceMap,
            final String sourceCode, final String key)
    {
        Integer index = sourceCode.indexOf(key);

        while (index >= 0)
        {
            occurrenceMap.put(index, key);
            index = sourceCode.indexOf(key, index + 1);
        }
    }

    /**
     * @param indentLevel Indent level.
     * 
     * @return the indent string for the given parameter.
     */
    private String getIndent(final int indentLevel)
    {
        String answer = "";

        if (indentLevel == 1)
        {
            answer = getIndent1();
        }
        else if (indentLevel > 1)
        {
            answer = getIndent1() + getIndent(indentLevel - 1);
        }

        return answer;
    }

    /**
     * @param sourceCode Source code.
     * @param indentLevel Indent level.
     * 
     * @return the given source code indented to the given level.
     */
    private String indent(final String sourceCode, final int indentLevel)
    {
        String answer = sourceCode;

        final String[] interiorLines = answer.split(LINE_SEPARATOR);

        if (interiorLines.length > 0)
        {
            final StringBuilder sb = new StringBuilder();
            final String indent = getIndent(indentLevel);

            for (int i = 0; i < interiorLines.length; i++)
            {
                sb.append(indent);
                sb.append(interiorLines[i]);
                sb.append(LINE_SEPARATOR);
            }

            answer = sb.toString();
        }

        return answer;
    }

    /**
     * Find occurrences of key that are not followed by a return, and insert
     * one.
     * 
     * @param sourceCode Source code.
     * @param key Key.
     * 
     * @return a formatted string.
     */
    private String maybeInsertFollowingReturns(final String sourceCode,
            final String key)
    {
        String answer = sourceCode;

        int index0 = answer.indexOf(key);

        while (index0 >= 0)
        {
            final int index1 = index0 + key.length();

            if (((index1 < answer.length()) && (answer.charAt(index1) != 10))
                    || (index1 == (answer.length())))
            {
                final int right = answer.length() - index1;

                final StringBuilder sb = new StringBuilder();
                sb.append(StringUtils.left(answer, index1));
                sb.append(LINE_SEPARATOR);
                sb.append(StringUtils.right(answer, right));

                answer = sb.toString();
            }

            index0 = answer.indexOf(key, index0 + 1);
        }

        return answer;
    }

    /**
     * Find occurrences of key that are not preceded by a return, and insert
     * one.
     * 
     * @param sourceCode Source code.
     * @param key Key.
     * 
     * @return a formatted string.
     */
    private String maybeInsertPrecedingReturns(final String sourceCode,
            final String key)
    {
        String answer = sourceCode;

        int index = answer.indexOf(key);

        while (index >= 0)
        {
            if ((index > 0) && (answer.charAt(index - 1) != 10))
            {
                final int right = answer.length() - index;

                final StringBuilder sb = new StringBuilder();
                sb.append(StringUtils.left(answer, index));
                sb.append(LINE_SEPARATOR);
                sb.append(StringUtils.right(answer, right));

                answer = sb.toString();
                index++;
            }

            index = answer.indexOf(key, index + 1);
        }

        return answer;
    }

    /**
     * @param sourceCode Source code.
     * 
     * @return the indented source code.
     */
    private String performIndentation(final String sourceCode)
    {
        final Map<Integer, String> occurrenceMap = new TreeMap<Integer, String>();
        collectOccurrences(occurrenceMap, sourceCode, INDENT_KEY0);
        collectOccurrences(occurrenceMap, sourceCode, INDENT_KEY1);
        occurrenceMap.put(0, "");
        occurrenceMap.put(sourceCode.length() - 1, "");

        final List<String> sections = new ArrayList<String>();
        final List<Integer> indices = new ArrayList<Integer>();
        indices.addAll(occurrenceMap.keySet());

        if (indices.get(indices.size() - 1) == (sourceCode.length() - 1))
        {
            indices.remove(indices.size() - 1);
        }

        int indentLevel = 0;

        for (int i = 0; i < (indices.size() - 1); i++)
        {
            Integer index0 = indices.get(i);
            index0 += occurrenceMap.get(index0).length();

            Integer index1 = indices.get(i + 1);
            final String key1 = occurrenceMap.get(index1);
            index1 += key1.length();

            String section = sourceCode.substring(index0, index1);
            section = indent(section, indentLevel);

            section = StringUtils.replace(section, getIndent1() + INDENT_KEY1,
                    INDENT_KEY1);
            section = StringUtils
                    .remove(section, getIndent1() + LINE_SEPARATOR);

            if (section.endsWith(LINE_SEPARATOR + LINE_SEPARATOR))
            {
                section = StringUtils.chomp(section, LINE_SEPARATOR);
            }

            sections.add(section);

            if (INDENT_KEY0.equals(key1))
            {
                indentLevel++;
            }
            else if (INDENT_KEY1.equals(key1))
            {
                indentLevel--;
            }
        }

        // Assemble sections.
        final StringBuilder sb = new StringBuilder();

        for (final String section : sections)
        {
            sb.append(section);
        }

        return sb.toString();
    }
}
